feat(linode): bake fjb-iptables FORWARD chain into cache cloud-init (FJB-98)#109
Merged
Conversation
…FJB-98) Cache nanodes provisioned by fj-bellows in cache-gateway mode now come up with the FJB-FORWARD chain installed at first boot, persisted via iptables-persistent. Previously the chain was only rendered into a log line (`wgboot: cache iptables re-rendered (TODO: push via SSH)`) and had to be hand-installed — the persistent test nanode for FJB-91 was the only place this happened, and a rebuild would lose the FORWARD policy entirely. What lands: - managedCache learns the worker VPC CIDR via setHardwareContext, then renders cachegateway.RenderCacheIPTables at cache-create time and embeds the script in cloud-init under /usr/local/sbin/fjb-iptables.sh. - cache-cloud-init.yaml.tmpl conditionally installs iptables-persistent, writes the script, runs it from runcmd, and saves via netfilter-persistent. ssh-mode renders byte-identical to before. - cachegateway Inputs.CacheVPCIP is now optional: the renderer never used it in the script body, and the bake-in happens before any VPC IP has been assigned. wgboot still passes it (eager Status lookup). ACL-derived per-prefix ACCEPT rules are intentionally omitted from the bake-in: the ACL registry isn't wired until wgboot starts (post-Configure), so the script that lands at create time is the skeleton (sysctl + ESTABLISHED,RELATED + FJB-92 reverse-direction + DROP). Runtime push of ACL-updated rules is the wgboot TODO already noted in code; it lands with FJB-99's ephemeral-cache bootstrap loop. Tests cover the cache-gateway and ssh-mode render branches and the renderIPTablesForCacheGateway transport-mode guard. Verified end-to-end against live Linode via test/e2e-linode/run-local.sh --transport=cache-gateway. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
* feat(agent): proto + ConnectRPC skeleton with Health RPC (FJB-94) (#107) * feat(agent): proto + ConnectRPC skeleton with Health RPC (FJB-94) First slice of the fjbagent foundation. Lands the proto package, the binary skeleton, and the Health RPC — no deploy story yet (cloud-init, systemd, cache base-image come in subsequent slices), no consumer code in the orchestrator yet (agent.Dialer is its own slice). - proto/fjbellows/agent/v1/agent.proto: AgentService.Health - cmd/fjbagent: static binary, ConnectRPC server, bearer auth, /healthz shim. -listen and -token-file flags (operator sets these from the systemd unit when deployment lands). - internal/agent: Handler captures build version + start time + hostname at construction; bearer middleware matches the internal/control auth shape (constant-time compare, /healthz exempt). - Unit tests: Health response shape, bearer accept/reject, /healthz open without token, graceful shutdown, LoadToken validation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(agent): split main into run() so defer cancel() runs on error (FJB-94) gocritic flagged exitAfterDefer: os.Exit bypasses deferred cancel(), so the signal-context goroutine leaks on the error path. Moving the work into run() that returns error lets the defer fire before main maps the error to an exit code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(agent): Exec bidi RPC, pipe-mode handler (FJB-93 Phase A) (#108) First slice of FJB-93. Lands the AgentService.Exec bidi-streaming RPC on the agent and a pipe-mode handler (no PTY yet — that's Phase C). The orchestrator-side ShellSession proxy and the fjbctl ssh CLI come in Phase B; the wire envelope shape (ShellMsg / ShellEvent) is already designed to support both phases without a re-do. Wire shape: - ShellMsg: oneof Open | Stdin | Resize | Signal | CloseStdin - ShellEvent: oneof Opened | Stdout | Stderr | Exit - ExecOpen: target (set by fjbctl, stripped by orchestrator), argv, env, tty (rejected with CodeUnimplemented this phase), term, winsize - ExecExit: code on normal exit, signal name on signal kill Handler behavior: - First frame must be Open or returns CodeInvalidArgument. - tty=true returns CodeUnimplemented. - argv exec'd directly (no shell interpolation); env is exact (no agent-env passthrough, matches the design doc). - exec.Cmd.Stdout/Stderr go through a streamWriter that feeds a buffered sendBuf channel; a single sender goroutine owns the bidi stream's Send side so we never need to mutex Connect's BidiStream. - cmd.Wait blocks until both child exit AND exec's internal copy goroutines have drained, so all output frames precede Exit by construction (avoids the StdoutPipe-vs-Wait race). - Child runs in a fresh process group (Setpgid); signals are delivered to the whole group via kill(-pgid). - Command-not-found surfaces as on-stream Exit{127} (sh convention); signal kills surface as Exit{code: 128+signum, signal: <name>}. Tests: 11 cases covering echo, stdin pump + CloseStdin, non-zero exit, stderr capture, env passthrough, tty/empty-argv rejection, command-not-found, signal kill, first-frame-must-be-Open, bearer auth gating. `go test -race -count=10` clean. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(linode): cache cloud-init WG bootstrap — Phase A (FJB-99) Foundation for the per-deployment ephemeral cache flow that FJB-91 called out as missing: each cache provisioned by fj-bellows now comes up with its own WG keypair, brings up wg-quick@wg0 referencing the orchestrator's pubkey, and publishes its own pubkey to S3 so the orchestrator can read it back in Phase B. What lands (Phase A): - main.go loads or generates the orchestrator's WG keypair BEFORE prov.Configure runs (was deferred until wgboot.Boot). Pubkey is pushed into the Linode provider via SetOrchestratorWGPubkey for the cache cloud-init injector. - managedCache carries orchestratorWGPubkey and renders four new fields into cacheCloudInitParams (OrchestratorWGPubkey, Orchestrator- WGAddr, CacheWGAddr, WGListenPort) when transport.mode is cache-gateway. - cache-cloud-init.yaml.tmpl conditionally installs wireguard + awscli, drops /usr/local/sbin/fjb-wg-bootstrap.sh, and runs it from runcmd. The script: - generates a Curve25519 keypair on first boot (idempotent — re- runs reuse the persisted private key); - writes /etc/wireguard/wg0.conf with the orchestrator pubkey baked in as [Peer].PublicKey and the cache's address pinned at 100.64.0.2/32; - brings up wg-quick@wg0; - publishes /etc/wireguard/publickey to s3://<bucket>/wg-pubkey.txt using the same Object Storage credentials already shared with zot — no new identity to provision. - ssh-mode renders byte-identical to before (the WG bootstrap block is gated on OrchestratorWGPubkey). Out of Phase A (lands in Phase B/C): - Orchestrator-side WaitForWGPubkey polling against the bucket + feeding the discovered pubkey into wgboot.Boot as the peer config. - bootWGStack ordering reorder (today still wgboot before cache.Ensure; Phase B moves it after). - e2e harness reactivation: drop the persistent-cache preflight, use the ephemeral cache + bootstrap, and reactivate the worker readiness + job-complete checks. - Drop the static transport.wg.peer.{public_key,endpoint} config knobs. Tests cover the cache-gateway and ssh-mode render branches; verified end-to-end against live Linode via test/e2e-linode/run-local.sh --transport=cache-gateway (stack-up scope from FJB-91 still passes — the new code path is exercised but worker validation is the Phase C deliverable). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
cachegateway.RenderCacheIPTablesoutput into the cache cloud-init at create time so freshly-built caches come up with the FJB-FORWARD chain installed + persisted (was a wgboot TODO; persistent test cache was hand-patched).setHardwareContextso the renderer has what it needs at create time.iptables-persistent, writes/usr/local/sbin/fjb-iptables.sh, runs it, and saves vianetfilter-persistent. ssh-mode renders byte-identical to before.cachegateway.Inputs.CacheVPCIPto optional — the renderer never used it in the script body, and at bake-in time the cache doesn't have a VPC IP yet. wgboot still passes it.What's NOT in this PR
Test plan
go build ./...go test ./...golangci-lint runbash test/e2e-linode/run-local.sh --transport=cache-gateway→ ALL OK (live Linode)🤖 Generated with Claude Code